In [74]:
    
! pip install typeguard rollbar returns tenacity > /dev/null 2>&1
    
In [47]:
    
import contextlib
import json
import logging
import pathlib
import os
from typing import Union
import requests
from typeguard import typechecked
    
In [48]:
    
# Naive code snippets
def get_relevant_restaurants(user):
    base_url = "https://en.wikipedia.org/wiki"
    return requests.get(f"{base_url}/{user}").content
def get_user(path):
    with open(path, 'r') as json_file:
        return json.load(json_file)["user"]
def pick_best_restaurants(restaurants):
    pass
    
In [49]:
    
def get_restaurant_recommendation(path):
    user = get_user(path)
    candidates = get_relevant_restaurants(user)
    return pick_best_restaurants(candidates)
    
get_restaurant_recommendation("MY_AMAZING_FILE.json")
    
In [175]:
    
def get_restaurant_recommendation(path):
    try:
        user = get_user(path)
        candidates = get_relevant_restaurants(user)
        pick_best_restaurants(candidates)
    except BaseException:
        logging.error("VERY UNINFORMATIVE INFORMATION")
        raise BaseException("VERY UNINFORMATIVE INFORMATION")
    
generaly it’s better for a program to fail fast and crash than to silence the error and continue running the program.
The bugs that inevitably happen later on will be harder to debug since they are far removed from the original cause.
Just because programmers often ignore error messages doesn’t mean the program should stop emitting them.
In [176]:
    
def get_restaurant_recommendation(path):
    try:
        user = get_config(path)["user"]
    except FileNotFoundException:
        logging.error("VERY UNINFORMATIVE INFORMATION")
        raise
    except JSONDecodeError:
        logging.error("VERY UNINFORMATIVE INFORMATION")
        raise
    candidates = get_relevant_restaurants(user)
    pick_best_restaurants(candidates)
    
When a file does not exists or when the file is not a valid json we raise FileNotFoundException and JSONDecodeError and log it away
We will reraise the same exact exception that occured instead raising a generic exception and allow the invoker to handle them diffrently.
Altough this code is far from pretty is much safer, we added deafault patiserie and the invoker of this function can destinguise between the diffrent types of errors and handle them in a diffrent manner if needed.
In [172]:
    
def get_restaurant_recommendation(path):
    try:
        user = get_user(path)
    except (FileNotFoundException, JSONDecodeError):
        logging.error("VERY UNINFORMATIVE INFORMATION")
        raise
    else:
        candidates = get_relevant_restaurants(user)
        pick_best_restaurants(candidates)
    
In [171]:
    
def get_restaurant_recommendation(path):
    try:
        user = get_user(path)
    except (FileNotFoundException, JSONDecodeError):
        logging.error("VERY UNINFORMATIVE INFORMATION")
        raise
    else:
        candidates = get_relevant_restaurants(user)
        pick_best_restaurants(candidates)
    
Secondly we can use else clause which occur when the try block executed and did not raise an exception.
Thirdly, we use dictionary builtin function get which allow us to define default values.
In [55]:
    
def run_unstopable_animation():
    pass
    
This
In [56]:
    
try:
    os.remove('somefile.pyc')
except FileNotFoundError:
    pass
    
In [57]:
    
try:
    run_unstopable_animation()
except KeyboardInterrupt:
    pass
    
Becomes
In [58]:
    
from contextlib import suppress
with suppress(FileNotFoundError):
    os.remove('somefile.pyc')
    
In [59]:
    
from contextlib import suppress
with suppress(KeyboardInterrupt):
    run_unstopable_animation()
    
In [169]:
    
def get_user(path):
    with open(path, 'r') as json_file:
        return json.load(json_file)\
                   .get("user", "default_user")
    
def get_restaurant_recommendation(path):
    try:
        user = get_user(path)
    except (FileNotFoundException, JSONDecodeError):
        logging.error("VERY UNINFORMATIVE INFORMATION")
        raise
    else:
        candidates = get_relevant_restaurants(user)
        pick_best_restaurants(candidates)
    
In [170]:
    
def get_user(path):
    with open(path, 'r') as json_file:
        try:
            user = json.load(json_file)\
                       .get("user", "default_user")
        except (FileNotFoundException, JSONDecodeError):
            logging.error("VERY UNINFORMATIVE INFORMATION")
            raise
        else:
            return user
def get_restaurant_recommendation(path):
    user = get_user(path)                
    candidates = get_relevant_restaurants(user)
    pick_best_restaurants(candidates)
    
In [144]:
    
def get_user(path: Union[str, pathlib.PurePath]) -> str:
    with open(path, 'r') as json_file:
        try:
            data = json.load(json_file)
        except (FileNotFoundException, JSONDecodeError):
            logging.error("VERY INFORMATIVE INFORMATION")
            raise
        else:
            return data.get("user","default_user")
    
In [145]:
    
@typechecked
def get_user(path: Union[str, pathlib.PurePath]) -> str:
    with open(path, 'r') as json_file:
        try:
            data = json.load(json_file)
        except (FileNotFoundException, JSONDecodeError):
            logging.error("VERY INFORMATIVE INFORMATION")
            raise
        else:
            return data.get("user","default_user")
    
In [126]:
    
def get_relevant_restaurants(user):
    base_url = "cool_restaurants.com"
    resp = requests.get(f"{base_url}/{user}")
    resp.raise_for_status()
    return resp.content
    
In [127]:
    
def get_relevant_restaurants(user):
    base_url = "cool_restaurants.com"
    
    allowed_retries = 5
    for i in range(allowed_retries):
        try:
            resp = requests.get(f"{base_url}/{user}")
            resp.raise_for_status()
        except (requests.ConnectionError):
            if i == allowed_retries:
                raise
        else:
            return resp.content
    
In [128]:
    
from functools import wraps
def retry(exceptions, allowed_retries=5):
    def callable(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            for i in range(allowed_retries):
                try:
                    res = func()
                except exceptions:
                    continue
                else:
                    return res       
        return wrapped
    return callable
    
In [129]:
    
@retry(exceptions=requests.ConnectionError)
def get_relevant_restaurants(country):
    base_url = "cool_restaurants.com"
    resp = requests.get(f"{base_url}/{user}")
    resp.raise_for_status()
    return resp.content
    
In [147]:
    
import tenacity
@tenacity.retry(retry=tenacity.retry_if_exception_type(ConnectionError))
def get_relevant_restaurants(user):
    base_url = "cool_restaurants.com"
    resp = requests.get(f"{base_url}/{user}")
    resp.raise_for_status()
    return resp.content
    
In [89]:
    
import sys
import rollbar
rollbar.init("Super Secret Token")
def rollbar_except_hook(exc_type, exc_value, traceback):
    rollbar.report_exc_info((exc_type, exc_value, traceback))
    sys.__excepthook__(exc_type, exc_value, traceback)
    
sys.excepthook = rollbar_except_hook
    
    
Lets say we have ValueError and we want to recover in diffrent way between TooBig/TooSmall.
Python default behavior
In [159]:
    
try:
    1/0
except ZeroDivisionError:
    raise
    
    
Replace exception type with both traces
In [161]:
    
try:
    1/0
except ZeroDivisionError as e:
    raise Exception from e
    
    
Replace exception type with only one trace
In [162]:
    
try:
    1/0
except ZeroDivisionError as e:
    raise Exception from None
    
    
In [ ]:
    
def pick_best_restaurants(user: str, candidates: List[str]) -> List[str]:    
    validate_user(user)
    best_candicates = heapq.nlargest(5, valid_candidates)
    update_df(user, best_candicates)
    send_email()
    
In [174]:
    
def get_user(path):
    with open(path, 'r') as json_file:
        try:
            user = json.load(json_file)\
                       .get("user", "default_user")
        except (FileNotFoundException, JSONDecodeError):
            logging.error("VERY UNINFORMATIVE INFORMATION")
            raise
        else:            
            try:
                send_email(user)
            else :
                
            return user
    
    
through logging, reporting, and monitoring software.
In a world where regulation around personal data is constantly getting stricter,
In [90]:
    
def login(user):
    raise CommonPasswordException(f"password: {password} is too common")
    
That being said, errors, whether in code form or simple error response, are a bit like getting a shot — unpleasant, but incredibly useful. Error codes are probably the most useful diagnostic element in the API space, and this is surprising, given how little attention we often pay them.
In general, the goal with error responses is to create a source of information to not only inform the user of a problem, but of the solution to that problem as well. Simply stating a problem does nothing to fix it – and the same is true of API failures.